<?PHP
session_start();
$startTime = round(microtime(), 3);
require_once("table.class.php");
require_once("claxml.php");
require_once("inc/strings.en.php");

// We will store our databases in a little hashmap
$DB;

// For now, we put user data in here until sessions start working
$USER;

// Load up them tables, doggie.
$DB["student"] = new Table("db/students.db");
$DB["credit"] = new Table("db/credits.db");
$DB["class"] = new Table("db/classes.db");
$DB["prereq"] = new Table("db/prereqs.db");
$DB["confirmation"] = new Table("db/confirmations.db");
$DB["semester"] = new Table("db/semesters.db");

// Make some regex patterns
$REGEX_CLASSCODE = "/^[A-Za-z]+[-]?[0-9]+$/";
$REGEX_CRN = "/^[0-9]+$/";

// Let's get some commands.
$M = chooseOne($_POST, $_GET);
$action = (empty($M['act'])) ? false : $M['act'];
$resource = (empty($M['r'])) ? false : $M['r'];
$vars = (empty($M['q'])) ? false : $M['q'];
$sid = (empty($M['sid'])) ? false : $M['sid'];

if($sid != false) {
	echo SID;
	session_id($sid);
}

handleRequest($action, $vars, $resource);

// Some (not-so) helpful statistics!
$endTime = round(microTime(), 3);
$dt = $endTime - $startTime;

echo "\n\n<!-- Page took $dt seconds -->";

// Really only the credit tables saves.
$DB['credit']->save();

// A handy way to choose between $_GET and $_POST
function chooseOne($first, $second) {
	if(! empty($first)) return $first;
	if(! empty($second)) return $second;
}

/**
* This function is essentially the meat of the script. Given an action
* and a string of characters, it determines what the output of the XML
* must be.
*/
function handleRequest($action, $vars, $resource) {
	global $DB;
	$action = strtolower($action);
	$resource = strtolower($resource);
	
	claxml_header();
	if($action == "search")
	{
		// Pawn this task off to a general search handler
		generalSearchAction( $resource, $vars );
	}
	else if( $action == "auth" ) {
		// Handled by a different function
		loginAction( $vars );
	}
	else if( $action == "logout" ) {
		logoutAction( );
	}
	else if( $action == "register") {
		registerAction( $vars );
	}
	else if( $action == "drop" ) {
		dropAction( $vars );
	}
	else if( $action == "grab" ) {
		grabAction( $vars );
	}
	else if( $action == "classlist" ) {
		classListAction( $vars );
	}
	else if( $action == "semesterlist") {
		semesterListAction($vars);
	}
	else if( $action == false )
	{
		// The user didn't specify an action. What an idiot. He has no friends.
		claxml_error('NO_ACTION_SPECIFIED');
	}
	else
	{
		// This action doesn't exist, cap'n!!!
		claxml_error('UNKNOWN_ACTION', $action);
	}
	// There should be no more output after this... seriously don't do it.
}

/**
 *  Drop a friggin' class.
 */

function dropAction($q) {
	global $DB, $_STRINGS;
	if(loggedIn() == false) {
		claxml_error('NOT_LOGGED_IN');
		return;
	}

	$crns = explode(',', $q);

	if(count($crns) == 0) {
		claxml_error('ARGUMENT_REQUIRED');
		return;
	}

	$results = array( );

	foreach($crns as $crn) {
		if(ctype_digit($crn) == false) {
			claxml_error('INVALID_ARGUMENT', $crn);
			return;
		}
		$item = array( );
		$item['crn'] = $crn;

		if(classExists($crn) == false) {
			$item['msg'] = $_STRINGS['INVALID_CRN'];
			$item['result'] = '0';
		}
		else if(courseIsDroppable($crn) == false) {
			$item['msg'] = $_STRINGS['SEMESTER_DROP_LOCKED'];
			$item['result'] = '0';
		}
		else if(studentHasClass($_SESSION['cwid'], $crn) == false) {
			$item['msg'] = $_STRINGS['NOT_REGISTERED'];
			$item['result'] = '0';
		}
		else {
			doDrop($_SESSION['cwid'], $crn);
			$item['msg'] = $_STRINGS['SUCCESSFUL_DROP'];
			$item['result'] = '1';
		}

		$results[$crn] = $item;
	}

	claxml_result2list("attempt", $results);
}

/**
 * Get a friggin' list of semesters. Geez.
 */
function semesterListAction($q) {
	global $DB;

	if(loggedIn() == false) {
		claxml_error('NOT_LOGGED_IN');
		return;
	}

	$result = $DB['confirmation']->simpleSearch('cwid', $_SESSION['cwid']);

	// First fetch semesters which are addable
	$semesters = $DB['semester']->simpleSearch('add', '1');
	$prikey = $DB['semester']->getPrimaryKey();

	// Assume that the student has not registered for any of these
	foreach($semesters as $semester) {
		$semesters[ $semester[ $prikey ] ][ 'confirmed' ] = '0';
	}
	
	// Next grab arrays the user has explicitly registered for
	foreach($result as $sem) {
		$row = $DB['semester']->fetchID($sem['sid']);
		$row['confirmed'] = '1';

		$semesters[ $row[$prikey] ] = $row;
	}
	krsort($semesters);

	claxml_result2list('semester', $semesters);
}

/**
 * The user should be logged in and they would like a list of
 * their classes.
 */
function classListAction($q) {
	global $DB, $USER;

	// Log yerself in!
	if(loggedIn() == false) {
		claxml_error('NOT_LOGGED_IN');
		return;
	}

	// Add in any restrictors the user might have passed in.
	$restrictors = array();
	if(!empty($q)) {
		$q = trim($q);
		$conditions = explode(',', $q);

		foreach($conditions as $condition) {
			$tmp = explode(':', $condition);

			if(count($tmp) != 2) {
				claxml_error('INVALID_CLASSLIST_ARGUMENT');
				return;
			}

			$name = $tmp[0];
			$value = $tmp[1];

			$restrictors[$name] = $value;
		}
	}

	// Grab the class list
	$rows = getClassList($_SESSION['cwid'], $restrictors);

	claxml_result2list('class', $rows);
}

function registerAction( $q ) {
	global $USER, $_STRINGS;

	$crns = explode(',', $q);

	// Log yerself in!
	if(loggedIn() == false) {
		claxml_error('NOT_LOGGED_IN');
		return;
	}

	$cwid = $_SESSION['cwid'];

	// OK. Do it.
	$result = array();
	foreach($crns as $crn) {
		$item = array();
		$item['crn'] = $crn;
		if( ($fail = hasPrereqs($cwid, $crn)) !== true) {
			// The student needs additional prereqs.
			$item['result'] = "0";

			// Take the CRN list and output it to the user
			if(is_array($fail)) {
				$s = implode(', ', crnTransform($fail));
			}
			else {
				$s = crnTransform($fail);
			}

			$item['msg'] = "{$_STRINGS['NEED_PREREQ']}: $s";
		}
		else if( crnExists( $crn ) == false) {
			// The CRN does not exist.
			$item['result'] = "0";
			$item['msg'] = $_STRINGS['INVALID_CRN'];
		}
		else if( studentHasClass( $cwid, $crn) == true) {
			// Student has class already
			$item["result"] = "0";
			$item['msg'] = $_STRINGS['ALREADY_REGISTERED'];
		}
		else if( courseIsAddable($crn) == false) {
			// Semester not open for registration
			$item['result'] = "0";
			$item['msg'] = $_STRINGS['SEMESTER_ADD_LOCKED'];
		}
		else {
			// OK why not?
			$item['result'] = "1";
			$item['msg'] = $_STRINGS['SUCCESSFUL_REGISTRATION'];

			doAdd($cwid, $crn);
		}
		$result[$crn] = $item;
	}
	claxml_result2list("attempt", $result);
}

function grabAction( $q ) {
	$q = trim($q);

	if(strlen($q) == 0) {
		claxml_error('INVALID_GRAB_FORMAT');
		return;
	}

	$args = explode(',', strtolower($q));

	$values = array( );
	foreach($args as $name) {
		$name = trim($name);

		$values[$name] = getValue($name);
	}
	claxml_response($values);
}

function loginAction( $q ) {
	$q = trim($q);
	$args = explode(':', $q);

	if(count($args) != 2) {
		claxml_error('INVALID_LOGIN_FORMAT');
		return;
	}

	$username = $args[0];
	$password = $args[1];

	if( authUser($username, $password) == false ) {
		$values["result"] = "false";
		claxml_response($values);
	}
	else {
		$cwid = getUserCWID($username);

		// Funk with the session.
		session_regenerate_id();

		$_SESSION["username"] = $username;
		$_SESSION["cwid"] = $cwid;

		$values["result"] = "true";
		$values["session"] = session_id();
		$values["cwid"] = $cwid;
		$values["current-sid"] = getCurrentSemester($cwid);
		$values["semester-name"] = getSemesterName($values["current-sid"]);
		claxml_response($values);
	}
}

function logoutAction() {
	session_destroy();
	session_regenerate_id();
}

function generalSearchAction( $resource, $vars ) {
	global $_STRINGS, $DB;

	if( $resource === false || empty( $DB[$resource] ) ) {
		claxml_error('INVALID_RESOURCE', $resource);
		return;
	}

	$args = explode(',', $vars);
	$hashmap = array( );
	$searchTerms = array( );

	foreach($args as $arg)
	{
		$mappings = getMappings($arg);

		if($mappings === FALSE)
		{
			// We couldn't figure out what they're doing. Assume a genearal search.
			$searchTerms[] = $arg;
		}
		else
		{
			$keys = array_keys($mappings);

			foreach($keys as $key)
			{
				if($DB[$resource]->hasField($key) == false) {
					claxml_error('INVALID_FIELD_NAME', $key);
					return;
				}
			}
			$hashmap = array_merge($hashmap, $mappings);
		}

	}

	$result = $DB[$resource]->hashTermSearch($hashmap, $searchTerms);


	$cwid = $_SESSION['cwid'];
	if($resource == "class") {
		// A special case for class results: check if the student has that class.
		foreach($result as $row) {
			$crn = $row['crn'];
			if(studentHasClass($cwid, $crn) == true) {
				$row['registered'] = "1";
				$result[$crn] = $row;
			}
			else {
				$row['registered'] = "0";
				$result[$crn] = $row;
			}
		}
	}

	claxml_result2list($resource, $result );
}

////////////////////////////////////////////////////////
/*           NON-ACTION FUNCTIONS PLEASE *//////////////
////////////////////////////////////////////////////////

function getClassList($cwid, $restrictors) {
	global $DB;
	if(! is_array($restrictors) ) {
		$restrictors = array();
	}

	$result = $DB['credit']->simpleSearch('cwid', $cwid);

	$classes = array();
	foreach($result as $row) {
		$tmp_restrictors = $restrictors;
		$tmp_restrictors['crn'] = $row['crn'];

		$tmp = $DB['class']->hashSearch($tmp_restrictors);

		if(count($tmp) <= 0) continue;

		$tmp_keys = array_keys($tmp);
		$first_item = $tmp[ $tmp_keys[0] ];

		$classes[$row['crn']] = $first_item;
	}
	return $classes;
}

function classExists($crn) {
	return crnExists($crn);
}

function crnExists( $crn ) {
	global $DB;
	$result = $DB['class']->simpleSearch("crn", $crn);
	return count($result) > 0;
}

function getValue( $name ) {
	global $USER;
	if($name == "currentsemester") {
		return getCurrentSemester($_SESSION["cwid"]);
	}
	
	return "Undefined";
}

function authUser($username, $password)
{
	global $DB;

	// Grab the result where the field 'username' matched our provided value.
	$result = $DB['student']->simpleSearch('username', $username);
	
	// No users returned -> unsuccessful login
	if(count($result) == 0) return false;

	// Check the password of the first result (messy, but it works)
	// TODO: One day, make usernames unique
	$keys = array_keys( $result );
	$row = $result[ $keys[0] ];
	return $row['password'] == $password;
}

function getMappings($bit) {
	global $REGEX_CLASSCODE, $REGEX_CRN;
	$bit = trim($bit);
	$bits = explode(':', $bit);
	$mappings = array( );

	if(count($bits) == 2)
	{
		$mappings[$bits[0]] = $bits[1];
	}
	else if(preg_match($REGEX_CLASSCODE, $bit) > 0)
	{
		// Uppercase it, for at least here:
		$bit = strtoupper($bit);
		$mappings["dept"] = preg_replace("/[-]?[0-9]+$/", "", $bit);
		$mappings["code"] = preg_replace("/^[A-Za-z]+[-]?/", "", $bit);
	}
	else if(preg_match($REGEX_CRN, $bit) > 0)
	{
		$mappings["crn"] = $bit;
	}
	else
	{
		return false;
	}
	return $mappings;
}

function hasPrereqs($cwid, $crn) {
	global $DB;

	// These are the classes the student is required to take...
	$required = $DB['prereq']->simpleSearch("crn", $crn);

	// These are the classes he has taken...
	$obtained = $DB['credit']->simpleSearch("cwid", $cwid);

	// We'll increment this variable every time the CRN's match up.
	// At the end if $hits >= count( $required ), that means the student
	// has the necessary prereqs.
	$hits = 0;

	$failures = array();

	foreach($required as $prereq) {
		$wcrn = $prereq['dep'];
		$match = false;

		foreach($obtained as $completed) {
			$hcrn = $completed['crn'];

			if($hcrn == $wcrn) {
				$match = true;
			}

		}
		if($match == false) {
			$failures[] = $wcrn;
		}
	}

	if(count($failures) == 0) return true;
	return $failures;
}

// Is user with CWID confirmed for semester SID
function isConfirmed($cwid, $sid) {
	global $DB;
	$result = $DB['confirmation']->simpleSearch("cwid", $cwid);

	// If no results returned, they are definitely not confirmed for anything
	if(count($result) <= 0) return false;

	// Loop through and see if any matches are found
	foreach($result as $row) {
		if($row['sid'] == $sid) {
			return true;
		}
	}

	// Default to failure
	return false;
}


// Sort in ascending order and find the first one that
// we can apply an action to.
function getCurrentSemester( $cwid ) {
	global $DB;
	$result = $DB['confirmation']->simpleSearch("cwid", $cwid);

	// Oops they haven't confirmed. Return the latest semester.
	if(count($result) == 0) return getLatestSemester();

	// Sort ascending
	ksort($result);

	// Find the oldest one for which we can manipulate.
	foreach($result as $row) {
		$sid = $row['sid'];
		if(semesterIsDroppable($sid) || semesterIsAddable($sid)) {
			return $sid;
		}
	}

	return getLatestSemester();
}

function getLatestSemester() {
	global $DB;

	// Match everything
	$semesters = $DB['semester']->hashSearch( array() );

	// Sort it in high-to-low by primary key
	krsort($semesters);

	// Loop until we find a valid semester
	foreach($semesters as $row) {
		if($row['add'] == 1) return $row['id'];
	}

	return 0;
}

function getUserCWID($username) {
	global $DB;

	$result = $DB['student']->simpleSearch("username", $username);

	$keys = array_keys($result);
	return $result[ $keys[0] ][ "cwid" ];
}

function getSemesterName($sid) {
	global $DB;
	$result = $DB['semester']->simpleSearch("id", $sid);
	$row = $result[$sid];
	return $row['name'];
}

function getClass($crn) {
	global $DB;
	$result = $DB['class']->simpleSearch("crn", $crn);
	return $result;
}


function loggedIn() {
	return !empty($_SESSION['cwid']);
}

function courseIsDroppable($crn) {
	global $DB;
	$row = $DB['class']->fetchID($crn);
	return semesterIsDroppable($row['sid']);
}

function courseIsAddable($crn) {
	global $DB;
	$class = $DB['class']->fetchID($crn);
	return semesterIsAddable( $class['sid'] );
}

function semesterIsAddable($sid) {
	global $DB;
	$semester = $DB['semester']->fetchID( $sid );
	return $semester['add'] != "0";
}

function semesterIsDroppable($sid) {
	global $DB;
	$semester = $DB['semester']->fetchID( $sid );
	return $semester['drop'] != "0";
}

function studentHasClass($cwid, $crn) {
	global $DB;

	$classes = getClassList($cwid, array());

	foreach($classes as $class) {
		if($class['crn'] == $crn) return true;
	}
	return false;
}

function doDrop($cwid, $crn) {
	if(studentHasClass($cwid, $crn) == false) return false;

	global $DB;
	$values['cwid'] = $cwid;
	$values['crn'] = $crn;
	return $DB['credit']->delete($values);
}

function doAdd($cwid, $crn) {
	if(studentHasClass($cwid, $crn) == true) return false;

	global $DB;

	$values['cwid'] = $cwid;
	$values['crn'] = $crn;
	$values['status'] = "0";
	return $DB['credit']->insert($values);
}

/* Transform a CRN into CS-XXX */
function crnTransform($crns) {
	global $DB;
	if(is_array($crns)) {
		$classes = array( );
		foreach($crns as $crn) {
			$class = $DB['class']->fetchID($crn);
			$s = "{$class['dept']}-{$class['code']}";
			$classes[] = $s;
		}
		return $classes;
	}

	// Scalar case
	$class = $DB['class']->fetchID($crns);
	$s = "{$class['dept']}-{$class['code']}";
	return $s;
}

?>
